#include "area.hh"

#include <fstream>
#include <string>
#include <map>
#include <memory>
#include <vector>
#include <iostream>
#include <unordered_set>
#include <queue>
#include <algorithm>

Area::Area(const std::string& filepath) : 
    _pipeArray{},
    _pipeTypes{},
    _startPosition{-1, -1}, 
    _row{-1}, 
    _col{-1}
{
    std::ifstream infile {filepath};
    std::string line;

    std::vector<std::string> tokens{};
    while (std::getline(infile, line))
        tokens.emplace_back(std::move(line));
        
    this->_row = tokens.size();
    this->_col = tokens.front().size();

    for (int row = 0; row < tokens.size(); ++row)
    {
        std::vector<std::shared_ptr<Pipe>> pipes {};

        for (int col = 0; col < tokens[row].size(); ++col)
        {
            // Start position ?
            if (tokens[row][col] == 'S')
            {
                // Decl the pipe type and record the start position
                bool northPiped {false};
                if (row > 0)
                {
                    char northToken {tokens[row - 1][col]};
                    northPiped = (northToken == '|' || northToken == '7' || northToken == 'F');
                }

                bool southPiped {false};
                if (row < tokens.size() - 1)
                {
                    char southToken {tokens[row + 1][col]};
                    southPiped = (southToken == '|' || southToken == 'L' || southToken == 'J');
                }

                bool eastPiped {false};
                if (col < tokens[row].size() - 1)
                {
                    char eastToken {tokens[row][col + 1]};
                    eastPiped = (eastToken == '-' || eastToken == 'J' || eastToken == '7');
                }

                bool weatPiped {false};
                if (col > 0)
                {
                    char westToken {tokens[row][col - 1]};
                    weatPiped = (westToken == '-' || westToken == 'L' || westToken == 'F');
                }

                char token {'\0'};
                if (northPiped && southPiped && !eastPiped && !weatPiped)
                    token = '|';
                else if (!northPiped && !southPiped && eastPiped && weatPiped)
                    token = '-';
                else if (northPiped && !southPiped && eastPiped && !weatPiped)
                    token = 'L';
                else if (northPiped && !southPiped && !eastPiped && weatPiped)
                    token = 'J';
                else if (!northPiped && southPiped && eastPiped && !weatPiped)
                    token = 'F';
                else if (!northPiped && southPiped && !eastPiped && weatPiped)
                    token = '7';
                else
                {
                    std::cout << "Not an valid start position" << std::endl;
                    std::cout << northPiped << southPiped << eastPiped << weatPiped << std::endl;
                    std::exit(2);
                }

                pipes.emplace_back( PipeFactory::create(token, {row, col}) );
                this->_startPosition = Position{row, col};
            }
            else
                pipes.emplace_back( PipeFactory::create(tokens[row][col], {row, col}) );
        }

        this->_pipeArray.emplace_back( std::move(pipes) );
    }

    this->_pipeTypes.assign(this->_row, std::vector<PipeType>(this->_col, PipeType::Ground));
}

int Area::getFarthestStep()
{
    std::queue<std::shared_ptr<Pipe>> queue {};
    std::vector<std::vector<int>> stepArray{
        this->_pipeArray.size(), 
        std::vector<int>(this->_pipeArray.front().size(), -1)
    };

    stepArray[this->startX()][this->startY()] = 0;
    auto getStep = [&stepArray] (const Position& pos) -> int {
        return stepArray[pos.first][pos.second];
    };

    auto setStep = [&stepArray] (const Position& pos, int step) {
        stepArray[pos.first][pos.second] = step;
    };

    queue.push( this->pipe(this->_startPosition) );

    int step {1};
    int farthestStep {0};
    while (!queue.empty())
    {
        std::unordered_set<std::shared_ptr<Pipe>> pipes{};

        while (!queue.empty())
        {
            std::shared_ptr<Pipe> pipe {queue.front()};
            this->_pipeTypes[pipe->position().first][pipe->position().second] = pipe->type();
            // std::cout << pipe->position().first << ' ' << pipe->position().second << std::endl;
            queue.pop();
            std::vector<Position> nextPositions {pipe->nextPositions()};
            for (const Position& pos : nextPositions)
            {
                // A new pipe!
                if (isPosInvalid(pos) && getStep(pos) == -1)
                {
                    setStep(pos, step);
                    pipes.emplace(this->pipe(pos));
                    farthestStep = step;
                }
            }
        }

        for (auto pipe : pipes)
            queue.push(pipe);

        step += 1;
    }

    return farthestStep;
}

int Area::enclosedTiles()
{
    int size {0};
    std::for_each(this->_pipeTypes.begin(), this->_pipeTypes.end(), [&size] (const std::vector<PipeType>& pipes) {
        bool enclosed {false};
        bool isNorth {true};
        for (PipeType pipe : pipes)
        {
            switch (pipe)
            {
                case PipeType::Ground: if (enclosed) size += 1; break;

                case PipeType::Vertical: enclosed = !enclosed; break;
                case PipeType::Horizontal: break;

                case PipeType::NorthEast: isNorth = true; break;
                case PipeType::SouthEast: isNorth = false; break;

                case PipeType::SouthWest: if (isNorth)  enclosed = !enclosed; break;
                case PipeType::NorthWest: if (!isNorth) enclosed = !enclosed; break;
            }
        }
    });

    return size;
}

void Area::print() const
{
    for (const std::vector<std::shared_ptr<Pipe>>& pipes : this->_pipeArray)
    {
        for (const std::shared_ptr<Pipe>& pipe : pipes)
            std::cout << pipe->token();
        std::cout << std::endl;
    }
}

std::shared_ptr<Pipe> 
Area::pipe(const Position& pos) const
{
    return this->_pipeArray[pos.first][pos.second];
}

int Area::startX() const 
{ return this->_startPosition.first; }

int Area::startY() const
{ return this->_startPosition.second; }

bool Area::isPosInvalid(const Position& pos) const
{
    if (pos.first < 0 || pos.second < 0)
        return false;
    if (pos.first >= this->_row || pos.second >= this->_col)
        return false;
    return true;
}